起因是在看《前端面試寶典之 React 篇》影片,冒出「渲染劫持」這個詞,追問下去發現它其實是 HOC 的一種實作技巧,而且是已經被 React 官方淡出的舊派做法。第二個問題問得很好:「render 又不是 React 專有的,你是不是要講三大框架?」——答案是三個框架各有自己的複用機制,名字完全不同。
關聯筆記:[[高階函式與函數式範式-取代OOP三大設計模式]] 講「函式當參數與回傳值」這個底層骨架,HOC 就是它在組件層的投影,也就是 Decorator 裝飾者模式;[[00-前端框架比較-Vue-React-Angular難易度與優缺點]] 講三框架的整體取捨,這篇聚焦在「邏輯複用」這一個切面。
(a) HOC(Higher-Order Component,高階組件):一個「接收組件、回傳新組件」的函式。名字借自數學的高階函式(Higher-Order Function),差別只在處理的單位從函式變成組件。
(b) 渲染劫持(Render Hijacking):在原組件真正被畫出來之前,攔截並修改它的渲染結果。這是 HOC 的一項技巧,不是另一個獨立概念。
(c) 反向繼承(Inheritance Inversion,II):HOC 的兩種實作流派之一。另一種是屬性代理(Props Proxy,PP)。
[!note] 為什麼叫「反向」
正常繼承是「子類別繼承父類別」。這裡 HOC 是包裝者,理應是外層,卻反過來去extends被包裝的組件,讓包裝者變成子類別,包裝關係跟繼承關係方向相反,所以叫反向繼承。
(d) 關鍵:class HOC extends WrappedComponent。HOC 繼承了原組件,於是它拿得到原組件的 render、state、生命週期方法。
(e) 在 HOC 自己的 render() 裡呼叫 super.render(),就能拿到原組件產出的 React 元素樹(Virtual DOM),接著讀取、修改或整個覆蓋掉。
function withRenderHijack(WrappedComponent) {
return class extends WrappedComponent {
render() {
// super.render() 回傳原組件的 JSX 元素樹
const elementTree = super.render();
if (!this.props.isAllowed) {
return <p>沒有權限</p>; // 整個覆蓋
}
return React.cloneElement(elementTree, {
className: 'hijacked', // 修改屬性
});
}
};
}
(f) 逐行拆解:
function withRenderHijack(WrappedComponent):慣例上 HOC 命名用 with 開頭,參數是「被包的組件」。return class extends WrappedComponent:回傳一個匿名 class,extends 建立原型鏈(prototype chain),讓這個新 class 的實例可以往上找到 WrappedComponent.prototype 上的方法。super.render():super 指向父類別的原型,所以這句是「呼叫父類別版本的 render,但 this 仍然是我自己」,因此原組件的 this.state 與 this.props 都讀得到。React.cloneElement(elementTree, {...}):React 元素是不可變(immutable)的,不能直接改屬性,只能複製一份並覆寫。(g) 條件式渲染:依 props 或 state 決定要不要渲染 super.render() 的內容。權限控制、Loading 狀態都屬於這一類。
(h) 修改/擴充元素樹:攔截回傳的 JSX,替換特定子元件、動態加 style / className、綁額外的事件監聽。
(i) 操縱 Props:在元素正式渲染前,攔截並替換子元件收到的 props。
(j) 「渲染」是所有框架的通用概念,但「渲染劫持」這個做法只在 React 成立,原因是 React 的 JSX 本質上就是 JavaScript 函式呼叫與物件,super.render() 回傳的是一個普通 JS 物件樹,你當然可以拿來讀跟改。Vue 的 template 與 Angular 的 template 在編譯後被框架接管,開發者拿不到那個中間結構。
(k) 三框架對照:
| React | Vue | Angular | |
|---|---|---|---|
| 主要複用機制 | HOC、Render Props、Custom Hooks(現代主流) | Composition API(Composables)、Mixins、Scoped Slots | 依賴注入(DI)、結構型指令(*ngIf)、裝飾器 |
| 能不能改元素樹 | 可以,JSX 就是 JS 物件 | 一般不行,template 由編譯器接管(除非自己寫 Render Function) | 不直接改,改用 ViewContainerRef 操作視圖容器 |
| 心智模型 | 包一層函式 | 抽一支組合函式 | 注入一個服務或指令 |
(l) 面試提醒:如果被問到 HOC,記得補一句「React 16.8 之後官方推薦用 Custom Hooks 取代大部分 HOC 場景」。反向繼承因為會破壞組件的靜態方法繼承、讓 ref 難以傳遞、也不利於 React DevTools 除錯,實務上已經很少用。知道它是為了讀懂舊 codebase 與應付面試,不是為了拿來寫新專案。
同層的 hoc-props-proxy-vs-inheritance-inversion.jsx 把兩種 HOC 流派並排寫出來對照。
| 題號 | 題名 | 為什麼相關 |
|---|---|---|
| 2666 | Allow One Function Call | 最小的「包一層、加行為」練習,HOC 的骨架 |
| 2623 | Memoize | 包一層並保留狀態,等同 HOC 快取 props 的心智模型 |
| 2632 | Curry | 函式回傳函式,理解高階函式的閉包基礎 |
| 2704 | To Be Or Not To Be | 回傳一個帶方法的物件,練習包裝者思維 |